package de.suur.jarmi.client;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.suur.jarmi.common.JarmiException;
import de.suur.jarmi.common.Response;
import de.suur.jarmi.common.Request;

/**
 * As the name suggests this class manages the connection to the RPC server. All
 * data is sent end received with ObjectInputStream and ObjectOutputStream and
 * compressed using the ZipInputStream and ZipOutputStream. If there is no
 * connection to the service the ConnectionManager automatically tries to
 * establish a new one every five seconds.
 *
 * @author Martin Nonnenmacher
 */
public class ConnectionManager extends Thread {

	private static final Logger log = LoggerFactory
			.getLogger(ConnectionManager.class);

	private volatile boolean connected;
	private String host;
	private int port;
	private volatile Socket socket;
	private ZipInputStream zin;
	private ZipOutputStream zout;
	private boolean reconnect = false;

	/**
	 * Create a new connection manager. You have to call start to establish a
	 * connection.
	 *
	 * @param host
	 * @param port
	 * @param logic
	 */
	public ConnectionManager(String host, int port) {
		this.host = host;
		this.port = port;
		setConnected(false);
	}

	/**
	 * Try to establish a new connection to the server.
	 *
	 * @return
	 */
	private boolean connect() {
		if (!connected) {
			try {
				log.debug("Trying to connect to {}:{}", host, port);
				if (socket != null && socket.isConnected())
					socket.close();
				socket = new Socket(host, port);
				if (socket.isConnected()) {
					log.debug("Connect succeeded");
					setConnected(true);
					return true;
				} else {
					log.debug("Connect failed");
					return false;
				}
			} catch (UnknownHostException e) {
				log.warn("Host '{}' unknown", host);
				setConnected(false);
			} catch (IOException e) {
				log.warn("Could not connect to host '{}' on port {}", host,
						port);
				setConnected(false);
			}
		}
		return false;
	}

	/**
	 * Returns if the client is currently connected to the server.
	 *
	 * @return
	 */
	public boolean isConnected() {
		return connected;
	}

	public boolean isReconnect() {
		return reconnect;
	}

	/**
	 * Send a new request to the server and wait for the reply. This method
	 * blocks until a reply is received and therefore should not be run in the
	 * Android GUI thread. It is only used by the remote service
	 * implementations.
	 *
	 * @param request
	 * @return
	 * @throws JarmiException
	 */
	synchronized Response request(Request request) throws JarmiException {
		try {
			if (connected) {
				if (zout == null) {
					zout = new ZipOutputStream(socket.getOutputStream());
					zout.setMethod(ZipOutputStream.DEFLATED);
					zout.setLevel(9);
				}
				if (zin == null) {
					zin = new ZipInputStream(socket.getInputStream());
				}

				// send request
				ZipEntry entry = new ZipEntry(Double.toString(System
						.currentTimeMillis()));
				ByteArrayOutputStream bos = new ByteArrayOutputStream();
				ObjectOutputStream out = new ObjectOutputStream(bos);
				out.writeObject(request);
				out.close();
				zout.putNextEntry(entry);
				zout.write(bos.toByteArray());
				bos.close();
				zout.closeEntry();

				// receive reply
				entry = zin.getNextEntry();
				bos = new ByteArrayOutputStream();
				int size;
				byte[] buffer = new byte[2048];
				while ((size = zin.read(buffer, 0, buffer.length)) != -1)
					bos.write(buffer, 0, size);
				bos.flush();
				ByteArrayInputStream bis = new ByteArrayInputStream(bos
						.toByteArray());
				ObjectInputStream in = new ObjectInputStream(bis);
				Response reply = (Response) in.readObject();

				return reply;
			} else
				return null;
		} catch (IOException e) {
			zin = null;
			zout = null;
			setConnected(false);
			throw new JarmiException("Could not send request " + request, e);
		} catch (ClassNotFoundException e) {
			zin = null;
			zout = null;
			setConnected(false);
			throw new JarmiException("Could not send request " + request, e);
		}
	}

	@Override
	public void run() {
		while (true) {
			try {
				if (isReconnect() && (socket == null || !socket.isConnected()))
					connected = false;
				if (!connected) {
					connect();
					Thread.sleep(5000);
				} else {
					Thread.sleep(10000);
				}
			} catch (InterruptedException e) {
			}
		}
	}

	/**
	 * Set the connectivity of the connection manager and update it in the
	 * StateInfo of the SmartBikeLogic.
	 *
	 * @param connected
	 */
	public void setConnected(boolean connected) {
		if (this.connected != connected)
			this.connected = connected;
	}

	/**
	 * Update the host and port settings of the connection. The current
	 * connection is closed.
	 *
	 * @param host
	 * @param port
	 */
	public void setHost(String host, int port) {
		this.host = host;
		this.port = port;
		setConnected(false);
	}

	public void setReconnect(boolean reconnect) {
		this.reconnect = reconnect;
	}
}
